// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.control.physics;

import bats.scene.Flames;
import bats.scene.SceneNode;
import bats.scene.WorldNode;
import bats.unit.UnitClass;
import com.jme3.audio.AudioNode;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.influencers.ParticleInfluencer;
import com.jme3.light.PointLight;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.math.MyMath;
import jme3utilities.physics.OverlapListener;
import jme3utilities.physics.SimpleGhostControl;

/**
 * A simple ghost control for a flame.
 * <p>
 * Each flame shrinks as it consumes fuel, its diameter varying as the cube root
 * of the remaining fuel. Meanwhile the flame's light also becomes dimmer. When
 * the fuel supply is exhausted, the control extinguished the flame.
 * <p>
 * Each instance is enabled at creation.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class BurnControl
        extends SimpleGhostControl
        implements OverlapListener {
    // *************************************************************************
    // constants

    /**
     * radius of illumination (in meters) for a standard-sized flame
     */
    final private static float standardLightRadius = 10f;
    /**
     * particle end size for a standard-sized flame
     */
    final private static float standardParticleEndSize = 0.001f;
    /**
     * particle start size for a standard-sized flame
     */
    final private static float standardParticleStartSize = 0.7f;
    /**
     * particle upward start speed (in meters per second) for a standard-sized
     * flame
     */
    final private static float standardParticleStartYSpeed = 3f;
    /**
     * downward particle acceleration (in meters per second squared) for a
     * standard-sized flame
     */
    final private static float standardParticleYGravity = 0.5f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(BurnControl.class.getName());
    // *************************************************************************
    // fields
    /**
     * rate at which the fuel is consumed (fuel units per second): set by
     * constructor
     */
    private float burnRate;
    /**
     * amount of fuel remaining (relative to a standard-sized flame): set by
     * constructor
     */
    private float remainingFuel;
    /**
     * which scene contains the flame: set by constructor
     */
    final private SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate an enabled control with a specified size and duration.
     *
     * @param relativeSize initial size relative to a standard-sized flame
     * (&gt;0)
     * @param lifeSeconds duration in seconds (&gt;0)
     * @param scene which scene contains the flame (not null)
     */
    public BurnControl(float relativeSize, float lifeSeconds, SceneNode scene) {
        super(true,
                scene.getWorld().getFlames().createCollisionShape(relativeSize),
                scene.getWorld().getPhysics().getSpace());

        assert relativeSize > 0f : relativeSize;
        assert lifeSeconds > 0f : lifeSeconds;
        remainingFuel = MyMath.cube(relativeSize);
        burnRate = remainingFuel / lifeSeconds;

        this.scene = scene;

        assert isEnabled();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Clone this control for a different spatial.
     *
     * @param spatial which spatial to clone for (not null)
     * @return a new control
     */
    @Override
    public Control cloneForSpatial(Spatial spatial) {
        throw new UnsupportedOperationException("Not supported yet.");
    }
    // *************************************************************************
    // GhostControl methods

    /**
     * Update the control. (Invoked once per frame.)
     *
     * @param simInterval simulation seconds since the previous update (&ge;0)
     */
    @Override
    public void update(float simInterval) {
        super.update(simInterval);

        assert simInterval >= 0f : simInterval;
        assert isEnabled();
        assert space != null;
        if (spatial == null) {
            return;
        }
        if (scene.isEnabled()) {
            /*
             * Burn a little fuel.
             */
            remainingFuel -= burnRate * simInterval;
            if (remainingFuel <= 0f) { // burned out
                logger.log(Level.INFO, "{0} burned out", spatial.getName());
                Flames flames = scene.getWorld().getFlames();
                flames.extinguish(spatial);
                return;
            }
        }
        /*
         * Update the components of the flame.
         */
        updateEmitter();
        updateCollisionShape();
        updateLight();
        updateSound();
    }
    // *************************************************************************
    // OverlapListener methods

    /**
     * React to an overlapping rigid body.
     *
     * @param overlappingBody the overlapping rigid body (not null)
     * @param overlappingSpatial the spatial of the overlapping rigid body (not
     * null)
     * @param localPoint the location of the overlap (rotated and translated to
     * this control's object, but at world scale)
     */
    @Override
    public void onOverlap(PhysicsRigidBody overlappingBody,
            Spatial overlappingSpatial, Vector3f localPoint) {
        assert overlappingBody != null;
        assert overlappingSpatial != null;
        assert localPoint != null;

        if (!isEnabled()) {
            return;
        }
        /*
         * Impair any unit which overlaps the flame's collision shape.
         */
        UnitClass units = scene.getWorld().getUnitClass(overlappingSpatial);
        if (units != null && !units.isImpaired(overlappingSpatial)) {
            logger.log(Level.INFO, "{0} burns {1}", new Object[]{
                spatial.getName(),
                overlappingSpatial.getName()
            });
            units.impair(overlappingSpatial);
        }
    }
    // *************************************************************************
    // private methods

    /**
     * Get the local coordinates of the flame's center.
     *
     * @return a new vector
     */
    private Vector3f getCenterLocal() {
        Flames flames = scene.getWorld().getFlames();
        float relativeSize = getRelativeSize();
        float y = flames.getCenterHeight(relativeSize) / scene.getScaleFactor();
        Vector3f local = new Vector3f(0f, y, 0f);
        return local;
    }

    /**
     * Get the world coordinates of the flame's center.
     *
     * @return a new vector
     */
    private Vector3f getCenterWorld() {
        Vector3f local = getCenterLocal();
        Vector3f world = spatial.localToWorld(local, null);
        return world;
    }

    /**
     * Get the relative size of the flame.
     *
     * @return size relative to a standard-size flame (&gt;0)
     */
    private float getRelativeSize() {
        assert remainingFuel >= 0f : remainingFuel;
        float relativeSize = MyMath.cubeRoot(remainingFuel);
        return relativeSize;
    }

    /**
     * Resize and reposition the physics control to reflect a change in the
     * flame's size.
     */
    private void updateCollisionShape() {
        Flames flames = WorldNode.find(spatial).getFlames();
        float relativeSize = getRelativeSize();
        CollisionShape shape = flames.createCollisionShape(relativeSize);
        changeShape(shape);
    }

    /**
     * Update the flame's emitter to reflect a change in the flame's size.
     */
    private void updateEmitter() {
        Node node = (Node) spatial;
        ParticleEmitter emitter = Flames.getEmitter(node);
        /*
         * Adjust emitter location.
         */
        Vector3f centerLocal = getCenterLocal();
        emitter.setLocalTranslation(centerLocal);
        /*
         * Adjust particle gravity.
         */
        float relativeSize = getRelativeSize();
        float scaleFactor = scene.getScaleFactor();
        float yGravity =
                standardParticleYGravity * relativeSize; // m/s^2 downward
        Vector3f gravity = new Vector3f(0f, -yGravity, 0f);
        gravity.divideLocal(scaleFactor);
        emitter.setGravity(gravity);
        /*
         * Adjust particle sizes.
         */
        float endSize = standardParticleEndSize * relativeSize; // meters
        emitter.setEndSize(endSize / scaleFactor);

        float startSize = standardParticleStartSize * relativeSize; // meters
        emitter.setStartSize(startSize / scaleFactor);
        /*
         * Update the influencer.
         */
        ParticleInfluencer influencer = emitter.getParticleInfluencer();
        float ySpeed = standardParticleStartYSpeed * relativeSize; // meters/s
        Vector3f velocity = new Vector3f(0f, ySpeed, 0f); // meters per second
        velocity.divideLocal(scaleFactor);
        influencer.setInitialVelocity(velocity);
    }

    /**
     * Update a flame's light to reflect a change in the flame's size.
     */
    private void updateLight() {
        PointLight light = (PointLight) scene.getLighting().find(spatial);
        /*
         * Update the radius of illumination.
         */
        float relativeSize = getRelativeSize();
        float scaleFactor = scene.getScaleFactor();
        float radius = standardLightRadius * relativeSize; // meters
        if (radius <= 0f) {
            /*
             * Zero has a very special meaning to setRadius().
             */
            radius = 1e-3f;
        }
        light.setRadius(radius / scaleFactor);
        /*
         * Update location.
         */
        Vector3f centerWorld = getCenterWorld();
        light.setPosition(centerWorld);
    }

    /**
     * Update the loudness of the flame's sound effect.
     */
    private void updateSound() {
        Node node = (Node) spatial;
        AudioNode crackleNode = (AudioNode) node.getChild(Flames.cracklePath);
        float loudness = 0f;
        if (scene.isEnabled()) {
            loudness = 0.3f * remainingFuel;
        }
        crackleNode.setVolume(loudness);
    }
}
